package com.bplow.deep.base.classload;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;

import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;


/* ------------------------------------------------------------ */
/** ClassLoader for HttpContext.
* Specializes URLClassLoader with some utility and file mapping
* methods.
*
* This loader defaults to the 2.3 servlet spec behavior where non
* system classes are loaded from the classpath in preference to the
* parent loader.  Java2 compliant loading, where the parent loader
* always has priority, can be selected with the 
* {@link org.eclipse.jetty.webapp.WebAppContext#setParentLoaderPriority(boolean)} 
* method and influenced with {@link WebAppContext#isServerClass(String)} and 
* {@link WebAppContext#isSystemClass(String)}.
*
* If no parent class loader is provided, then the current thread 
* context classloader will be used.  If that is null then the 
* classloader that loaded this class is used as the parent.
* 
*/
public class WebAppClassLoader extends URLClassLoader
{
private static final Logger LOG = Log.getLogger(WebAppClassLoader.class);

private final Context _context;
private final ClassLoader _parent;
private final Set<String> _extensions=new HashSet<String>();
private String _name=String.valueOf(hashCode());

/* ------------------------------------------------------------ */
/** The Context in which the classloader operates.
 */
public interface Context
{
    /* ------------------------------------------------------------ */
    /** Convert a URL or path to a Resource.
     * The default implementation
     * is a wrapper for {@link Resource#newResource(String)}.
     * @param urlOrPath The URL or path to convert
     * @return The Resource for the URL/path
     * @throws IOException The Resource could not be created.
     */
    Resource newResource(String urlOrPath) throws IOException;

    /* ------------------------------------------------------------ */
    /**
     * @return Returns the permissions.
     */
    PermissionCollection getPermissions();

    /* ------------------------------------------------------------ */
    /** Is the class a System Class.
     * A System class is a class that is visible to a webapplication,
     * but that cannot be overridden by the contents of WEB-INF/lib or
     * WEB-INF/classes 
     * @param clazz The fully qualified name of the class.
     * @return True if the class is a system class.
     */
    boolean isSystemClass(String clazz);

    /* ------------------------------------------------------------ */
    /** Is the class a Server Class.
     * A Server class is a class that is part of the implementation of 
     * the server and is NIT visible to a webapplication. The web
     * application may provide it's own implementation of the class,
     * to be loaded from WEB-INF/lib or WEB-INF/classes 
     * @param clazz The fully qualified name of the class.
     * @return True if the class is a server class.
     */
    boolean isServerClass(String clazz);

    /* ------------------------------------------------------------ */
    /**
     * @return True if the classloader should delegate first to the parent 
     * classloader (standard java behaviour) or false if the classloader 
     * should first try to load from WEB-INF/lib or WEB-INF/classes (servlet 
     * spec recommendation).
     */
    boolean isParentLoaderPriority();
    
    /* ------------------------------------------------------------ */
    String getExtraClasspath();
    
}

/* ------------------------------------------------------------ */
/** Constructor.
 */
public WebAppClassLoader(Context context)
    throws IOException
{
    this(null,context);
}

/* ------------------------------------------------------------ */
/** Constructor.
 */
public WebAppClassLoader(ClassLoader parent, Context context)
    throws IOException
{
    super(new URL[]{},parent!=null?parent
            :(Thread.currentThread().getContextClassLoader()!=null?Thread.currentThread().getContextClassLoader()
                    :(WebAppClassLoader.class.getClassLoader()!=null?WebAppClassLoader.class.getClassLoader()
                            :ClassLoader.getSystemClassLoader())));
    _parent=getParent();
    _context=context;
    if (_parent==null)
        throw new IllegalArgumentException("no parent classloader!");
    
    _extensions.add(".jar");
    _extensions.add(".zip");
    
    // TODO remove this system property
    String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions");
    if(extensions!=null)
    {
        StringTokenizer tokenizer = new StringTokenizer(extensions, ",;");
        while(tokenizer.hasMoreTokens())
            _extensions.add(tokenizer.nextToken().trim());
    }
    
    if (context.getExtraClasspath()!=null)
        addClassPath(context.getExtraClasspath());
}

/* ------------------------------------------------------------ */
/**
 * @return the name of the classloader
 */
public String getName()
{
    return _name;
}

/* ------------------------------------------------------------ */
/**
 * @param name the name of the classloader
 */
public void setName(String name)
{
    _name=name;
}


/* ------------------------------------------------------------ */
public Context getContext()
{
    return _context;
}

/* ------------------------------------------------------------ */
/**
 * @param resource Comma or semicolon separated path of filenames or URLs
 * pointing to directories or jar files. Directories should end
 * with '/'.
 */
public void addClassPath(Resource resource)
    throws IOException
{
    if (resource instanceof ResourceCollection)
    {
        for (Resource r : ((ResourceCollection)resource).getResources())
            addClassPath(r);
    }
    else
    {
        addClassPath(resource.toString());
    }
}

/* ------------------------------------------------------------ */
/**
 * @param classPath Comma or semicolon separated path of filenames or URLs
 * pointing to directories or jar files. Directories should end
 * with '/'.
 */
public void addClassPath(String classPath)
    throws IOException
{
    if (classPath == null)
        return;
        
    StringTokenizer tokenizer= new StringTokenizer(classPath, ",;");
    while (tokenizer.hasMoreTokens())
    {
        Resource resource= _context.newResource(tokenizer.nextToken().trim());
        if (LOG.isDebugEnabled())
            LOG.debug("Path resource=" + resource);

        // Add the resource
        if (resource.isDirectory() && resource instanceof ResourceCollection)
            addClassPath(resource);
        else
        {
            // Resolve file path if possible
            File file= resource.getFile();
            if (file != null)
            {
                URL url= resource.getURL();
                addURL(url);
            }
            else if (resource.isDirectory())
                addURL(resource.getURL());
            else
                throw new IllegalArgumentException("!file: "+resource);
        }
    }
}

/* ------------------------------------------------------------ */
/**
 * @param file Checks if this file type can be added to the classpath.
 */
private boolean isFileSupported(String file)
{
    int dot = file.lastIndexOf('.');
    return dot!=-1 && _extensions.contains(file.substring(dot));
}

/* ------------------------------------------------------------ */
/** Add elements to the class path for the context from the jar and zip files found
 *  in the specified resource.
 * @param lib the resource that contains the jar and/or zip files.
 */
public void addJars(Resource lib)
{
    if (lib.exists() && lib.isDirectory())
    {
        String[] files=lib.list();
        for (int f=0;files!=null && f<files.length;f++)
        {
            try 
            {
                Resource fn=lib.addPath(files[f]);
                String fnlc=fn.getName().toLowerCase(Locale.ENGLISH);
                // don't check if this is a directory, see Bug 353165
                if (isFileSupported(fnlc))
                {
                    String jar=fn.toString();
                    jar=StringUtil.replace(jar, ",", "%2C");
                    jar=StringUtil.replace(jar, ";", "%3B");
                    addClassPath(jar);
                }
            }
            catch (Exception ex)
            {
                LOG.warn(Log.EXCEPTION,ex);
            }
        }
    }
}

/* ------------------------------------------------------------ */
public PermissionCollection getPermissions(CodeSource cs)
{
    PermissionCollection permissions=_context.getPermissions();
    PermissionCollection pc= (permissions == null) ? super.getPermissions(cs) : permissions;
    return pc;
}

/* ------------------------------------------------------------ */
public Enumeration<URL> getResources(String name) throws IOException
{
    boolean system_class=_context.isSystemClass(name);
    boolean server_class=_context.isServerClass(name);
    
    List<URL> from_parent = toList(server_class?null:_parent.getResources(name));
    List<URL> from_webapp = toList((system_class&&!from_parent.isEmpty())?null:this.findResources(name));
        
    if (_context.isParentLoaderPriority())
    {
        from_parent.addAll(from_webapp);
        return Collections.enumeration(from_parent);
    }
    from_webapp.addAll(from_parent);
    return Collections.enumeration(from_webapp);
}

/* ------------------------------------------------------------ */
private List<URL> toList(Enumeration<URL> e)
{
    if (e==null)
        return new ArrayList<URL>();
    return Collections.list(e);
}

/* ------------------------------------------------------------ */
/**
 * Get a resource from the classloader
 * 
 * NOTE: this method provides a convenience of hacking off a leading /
 * should one be present. This is non-standard and it is recommended 
 * to not rely on this behavior
 */
public URL getResource(String name)
{
    URL url= null;
    boolean tried_parent= false;
    boolean system_class=_context.isSystemClass(name);
    boolean server_class=_context.isServerClass(name);
    
    if (system_class && server_class)
        return null;
    
    if (_parent!=null &&(_context.isParentLoaderPriority() || system_class ) && !server_class)
    {
        tried_parent= true;
        
        if (_parent!=null)
            url= _parent.getResource(name);
    }

    if (url == null)
    {
        url= this.findResource(name);

        if (url == null && name.startsWith("/"))
        {
            if (LOG.isDebugEnabled())
                LOG.debug("HACK leading / off " + name);
            url= this.findResource(name.substring(1));
        }
    }

    if (url == null && !tried_parent && !server_class )
    {
        if (_parent!=null)
            url= _parent.getResource(name);
    }

    if (url != null)
        if (LOG.isDebugEnabled())
            LOG.debug("getResource("+name+")=" + url);

    return url;
}

/* ------------------------------------------------------------ */
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException
{
    return loadClass(name, false);
}

/* ------------------------------------------------------------ */
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    Class<?> c= findLoadedClass(name);
    ClassNotFoundException ex= null;
    boolean tried_parent= false;
    
    boolean system_class=_context.isSystemClass(name);
    boolean server_class=_context.isServerClass(name);
    
    if (system_class && server_class)
    {
        return null;
    }
    
    if (c == null && _parent!=null && (_context.isParentLoaderPriority() || system_class) && !server_class)
    {
        tried_parent= true;
        try
        {
            c= _parent.loadClass(name);
            if (LOG.isDebugEnabled())
                LOG.debug("loaded " + c);
        }
        catch (ClassNotFoundException e)
        {
            ex= e;
        }
    }

    if (c == null)
    {
        try
        {
            c= this.findClass(name);
        }
        catch (ClassNotFoundException e)
        {
            ex= e;
        }
    }

    if (c == null && _parent!=null && !tried_parent && !server_class )
        c= _parent.loadClass(name);

    if (c == null)
        throw ex;

    if (resolve)
        resolveClass(c);

    if (LOG.isDebugEnabled())
        LOG.debug("loaded " + c+ " from "+c.getClassLoader());
    
    return c;
}

/* ------------------------------------------------------------ */
public String toString()
{
    return "WebAppClassLoader=" + _name+"@"+Long.toHexString(hashCode());
}
}

